package com.bugsnag.ohos;

import com.bugsnag.ohos.annotation.VisibleForTesting;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * Provides a service for submitting lengthy tasks to run on background threads.
 * <p>
 * A [TaskType] must be submitted with each task, which routes it to the appropriate executor.
 * Setting the correct [TaskType] is critical as it can be used to enforce thread confinement.
 * It also avoids short-running operations being held up by long-running operations submitted
 * to the same executor.
 *
 * @since 2021-07-02
 */
public class BackgroundTaskService {
    private static final long SHUTDOWN_WAIT_MS = 1500L;
    private static final int THREAD_POOL_SIZE = 1;
    private static final long KEEP_ALIVE_SECS = 30L;
    private static final int TASK_QUEUE_SIZE = 128;

    @VisibleForTesting
    ThreadPoolExecutor errorExecutor = createExecutor(
        "Bugsnag Error thread",
        true
    );

    @VisibleForTesting
    ThreadPoolExecutor sessionExecutor = createExecutor(
        "Bugsnag Session thread",
        true
    );

    @VisibleForTesting
    ThreadPoolExecutor ioExecutor = createExecutor(
        "Bugsnag IO thread",
        true
    );

    @VisibleForTesting
    ThreadPoolExecutor internalReportExecutor = createExecutor(
        "Bugsnag Internal Report thread",
        false
    );

    @VisibleForTesting
    ThreadPoolExecutor defaultExecutor = createExecutor(
        "Bugsnag Default thread",
        false
    );

    /**
     * Submits a task for execution on a single-threaded executor. It is guaranteed that tasks
     * with the same [TaskType] are executed in the order of submission.
     * <p>
     * The caller is responsible for catching and handling
     * [java.util.concurrent.RejectedExecutionException] if the executor is saturated.
     * <p>
     * On process termination the service will attempt to wait for previously submitted jobs
     * with the task type [TaskType.ERROR_REQUEST], [TaskType.SESSION_REQUEST] and [TaskType.IO].
     * This is a best-effort attempt - no guarantee can be made that the operations will complete.
     *
     * @param taskType
     * @param runnable
     * @return Future
     * @throws RejectedExecutionException
     */
    public Future<Object> submitTask(TaskType taskType, Runnable runnable) throws RejectedExecutionException {
        return submitTask(taskType, Executors.callable(runnable));
    }

    /**
     * submitTask
     *
     * @param taskType
     * @param callable
     * @param <T>
     * @return Future
     * @throws RejectedExecutionException
     */
    public <T> Future<T> submitTask(TaskType taskType, Callable<T> callable) throws RejectedExecutionException {
        if (taskType == TaskType.ERROR_REQUEST) {
            return errorExecutor.submit(callable);
        } else if (taskType == TaskType.ERROR_REQUEST) {
            return sessionExecutor.submit(callable);
        } else if (taskType == TaskType.IO) {
            return ioExecutor.submit(callable);
        } else if (taskType == TaskType.INTERNAL_REPORT) {
            return internalReportExecutor.submit(callable);
        } else if (taskType == TaskType.DEFAULT) {
            return defaultExecutor.submit(callable);
        }
        return defaultExecutor.submit(callable);
    }

    /**
     * Notifies the background service that the process is about to terminate. This causes it to
     * shutdown submission of tasks to executors, while allowing for in-flight tasks
     * to be completed within a reasonable grace period.
     */
    public void shutdown() {
        internalReportExecutor.shutdownNow();
        defaultExecutor.shutdownNow();
        errorExecutor.shutdown();
        sessionExecutor.shutdown();
        try {
            errorExecutor.awaitTermination(SHUTDOWN_WAIT_MS, TimeUnit.MILLISECONDS);
            sessionExecutor.awaitTermination(SHUTDOWN_WAIT_MS, TimeUnit.MILLISECONDS);
            ioExecutor.shutdown();
            ioExecutor.awaitTermination(SHUTDOWN_WAIT_MS, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            e.getMessage();
        }
    }

    ThreadPoolExecutor createExecutor(String name, boolean keepAlive) {
        BlockingQueue<Runnable> queue = new LinkedBlockingQueue(TASK_QUEUE_SIZE);
        ThreadFactory threadFactory = runnable -> new java.lang.Thread(runnable, name);
        int coreSize = 0;
        if (keepAlive) {
            coreSize = THREAD_POOL_SIZE;
        }
        return new ThreadPoolExecutor(
            coreSize,
            THREAD_POOL_SIZE,
            KEEP_ALIVE_SECS,
            TimeUnit.SECONDS,
            queue,
            threadFactory
        );
    }
}
